// Copyright 2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: Jeff Stewart
//
// Implements a probe to exercise POP over SSL on port 995.

#include <QtCore/QDir>
#include <QtCore/QMap>
#include <QtCore/QRegExp>
#include <QtCore/QTime>
#include <QtNetwork/QtNetwork>
#include <QtGui/QTextDocument>
#include <algorithm>

#include "probes/popprobe.h"
#include "probes/ssl.h"


namespace popdiag {

const int PopProbe::kTimeoutMs = 30000;

PopProbeFactory::PopProbeFactory(const QString &username,
                                 const QString &password,
                                 const QString &pop_server_name,
                                 bool pull_all_mail)
    : username_(username), password_(password),
      pop_server_name_(pop_server_name), pull_all_mail_(pull_all_mail) {
}

Probe *PopProbeFactory::CreateProbe(const QHostAddress &addr,
                                    bool do_complete_probe) {
  return new PopProbe(username_, password_, pop_server_name_,
                      addr, pull_all_mail_, do_complete_probe);
}

PopProbe::PopProbe(const QString &username, const QString &password,
                   const QString &pop_server, const QHostAddress &host_address,
                   bool pull_all_mail, bool do_complete_probe)
    : Probe("Pop"), username_(username), password_(password),
      pop_server_(pop_server),
      base_socket_(), socket_(), state_(INIT),
      max_state_(do_complete_probe ? RETR : BANNER),
      fault_(OK), pull_all_mail_(pull_all_mail),
      probe_banner_only_(!do_complete_probe), we_closed_(false),
      bytes_retrieved_(0),
      bytes_expected_(0),
      msgs_in_mailbox_(0), msgs_retrieved_(0), uidls_(),
      lengths_(), target_address_(host_address),
      connect_timer_(NULL), ssl_negotiate_timer_(NULL),
      fetch_timer_(NULL), reader_(NULL) {
}

PopProbe::~PopProbe() {
}

void PopProbe::SetState(ProbeState new_state) {
  CHECK_GE(max_state_, new_state);
  state_ = new_state;
  emit ProgressMade(new_state, max_state_);
}

void PopProbe::GetProgress(int *completed, int *estimated_total) {
  if (state_ == RETR && bytes_retrieved_ != 0 && bytes_expected_ != 0) {
    *completed = max_state_ + 10.0 * bytes_retrieved_ / bytes_expected_;
  } else {
    *completed = state_;
  }
  *estimated_total = max_state_ + 10;
}

int PopProbe::GetFaultId() {
  return fault_;
}

QString PopProbe::GetDisplayText() {
  switch (fault_) {
    case OK:
      return tr("The POP server appears to be working properly "
                "for your account.",
                "The POP test succeeded.");
    case RESOLVE_FAILED:
      return tr("The POP server does not resolve in DNS. "
                "Is the network connected?",
                "Resolving the hostname for the POP Server using the "
                "Domain Name System (DNS) failed. This can be a symptom "
                "that the computer is not connected to the internet.");
    case CONNECTION_REFUSED:
      return tr("The POP server refused our connection attempt.",
                "Establishing a TCP connection failed because the "
                "remote server refused to accept the connection.");
    case SERVER_CLOSED_PREMATURELY:
      return tr("The POP server closed the connection prematurely",
                "Some time during the POP protocol conversation, the server "
                "closed the TCP connection unexpectedly.");
    case SOCKET_ERROR:
      return tr("We were unable to use the network to "
                "connect to the POP server.",
                "Using the local Sockets API, we were unable to get a socket "
                "to use to connect to the POP server.");
    case POP_SERVER_CAFILE_NOT_FOUND:
      // file of acceptable SSL certifying authorities was not found where
      // we expected it.  We cannot evaluate SSL certificate validity.
      return tr("There is a problem with the installation of "
                "this program. (CAFile)",
                "A needed configuration file (CAFile) is missing from "
                "the installation.");
    case POP_SERVER_CERT_SELFSIGNED:
      return tr("The SSL certificate for the POP server is not valid."
                " It is self-signed, or a certificate in the certifying "
                "authority chain is self-signed.",
                "The SSL certificate presented by the server is unacceptable.");
    case POP_SERVER_CERT_EXPIRED:
      return tr("The SSL certificate for the POP server is expired.",
                "The SSL certificate presented by the server is unacceptable.");
    case POP_SERVER_CERT_NOTYETVALID:
      return tr("The SSL certificate for the POP server is not yet valid.",
                "The SSL certificate presented by the server is unacceptable.");
    case POP_SERVER_CERT_CANNOT_VALIDATE:
      return tr("We cannot validate the SSL certificate for the POP server.",
                "The SSL certificate presented by the server is unacceptable.");
    case POP_SERVER_CERT_NAME_MISMATCH:
      return tr("The SSL certificate for the POP server does "
                "not match the hostname we used to connect to it.",
                "The hostname presented in the SSL certificate is different "
                "than the hostname we used to connect to it.");
    case POP_SERVER_SHEDDING_LOAD:
      return tr("The POP server is overloaded and is turning "
                "away connections.",
                "We can't talk to the POP server because it is overloaded "
                "and turned us away.");
    case POP_SERVER_RESPONSE_MALFORMED:
      return tr("The POP server sent a malformed response to our command.",
                "The POP server sent a protocol response to our request "
                "that did not match our expectations.");
    case POP_CAPA_USER_MISSING:
      return tr("The POP server does not offer plaintext authentication.",
                "We need to login to the POP Server, but this POP Server will "
                "not accept the only form of authentication that we know "
                "about (presenting the username and password).");
    case POP_USER_REJECTED:
      return tr("The POP server rejected our username. "
                "This is generally a system problem.",
                "The POP Server failed on the USER command.");
    case POP_USER_BACKEND_OFFLINE:
      return tr("The POP server is unavailable for this specific user "
                "account due to a temporary system failure. "
                "Please try again later.",
                "A Gmail-specific error message was generated that "
                "indicates that a needed server is offline.");
    case POP_LOGIN_FAILED_TEMP_SYSTEM_FAILURE:
      return tr("The POP server could not process the login attempt for "
                "this user due to a temporary system failure. "
                "Please try again later.",
                "The login failed, but a status code indicates that it was "
                "due to a system failure that is expected to resolve itself "
                "in time.");
    case POP_NOT_ENABLED_FOR_USER:
      return tr("POP has not been enabled for this account "
                "in the web interface.",
                "Gmail POP must be enabled as an account preference. "
                "That has not been done for this account.");
    case POP_LOGIN_FAILED_PERM_SYSTEM_FAILURE:
      return tr("The POP server could not process the login attempt "
                "for this user. Please contact user support.",
                "The login failed and a status code indicates that the "
                "problem will not resolve itself without operator "
                "intervention.");
    case POP_LOGIN_FAILED_ABUSE:
      return tr("The POP server refused to service this account due to abuse.",
                "The login failed and a status code indicates that the "
                "problem is that the account is flagged for abuse.");
    case POP_LOGIN_FAILED_GEO_LOCKOUT:
      return tr("The POP server does not offer POP service to the part "
                "of the world associated with this IP address.",
                "The login failed and a status code indicates that this "
                "happened because the POP service is not offered to the part "
                "of the world the request comes from.");
    case POP_LOGIN_FAILED_BAD_CREDS:
      return tr("The POP server did not accept your username and password.",
                "The login failed and a status code indicates that the "
                "problem was the username and password presented "
                "did not match.");
    case POP_RETR_FAILED:
      return tr("The POP server would not return a "
                "requested message from your account.",
                "Login succeeded, but retrieving a message failed.");
    case POP_STAT_FAILED:
      return tr("The POP server could not answer a STAT request.",
                "STAT is a POP operation.  It failed.");
    case POP_UIDL_FAILED:
      return tr("The POP server could not answer a UIDL request.",
                "UIDL is a POP operation.  It failed.");
    case POP_LIST_FAILED:
      return tr("The POP server could not answer a LIST request.",
                "LIST is a POP operation.  It failed.");
    case CONNECT_TIMED_OUT:
      return tr("Connecting to the POP Server timed out.",
                "Establishing a TCP connection to the POP server failed "
                "to complete in a timely manner.");
    case POP_RESPONSE_TIMED_OUT:
      return tr("The POP server did not return a response to our command.",
                "A command timed out before receiving a response "
                "from the server.");
    case UNKNOWN:
      // fall through to default handling
      ;
  }
  CHECK(false);
  return tr("An unknown error occurred.", "This should NEVER happen.");
}

void PopProbe::StartProbe() {
  ClearLog();
  StartLogDetails();
  StartProbeConnect();
}

void PopProbe::Cleanup() {
  we_closed_ = true;
  if (base_socket_ != NULL) {
    // Do NOT delete the socket.  Just close it.
    // It's still in use on the stack below us, in all probability.
    base_socket_->close();
  }
}

void PopProbe::ProbeDone(PopProbeFault f) {
  if (we_closed_ || fault_ != OK) {
    return;
  }
  Cleanup();

  // First fault wins.
  EndLogDetails();
  fault_ = f;
  emit ProbeComplete(fault_ == OK ? PROBE_OK : PROBE_FAILED);
}

void PopProbe::StartProbeConnect() {
  SetState(CONNECT);

  // Set a connection failure timeout
  connect_timer_ = new QTimer(this);
  connect(connect_timer_, SIGNAL(timeout()),
          this, SLOT(OnConnectTimedOut()));
  connect_timer_->setSingleShot(true);
  connect_timer_->start(kTimeoutMs);

  base_socket_.reset(new RenamableSslSocket());
  CHECK(connect(base_socket_.get(),
                SIGNAL(error(QAbstractSocket::SocketError)),
                this, SLOT(OnConnectError(QAbstractSocket::SocketError))));
  CHECK(connect(base_socket_.get(), SIGNAL(connected()),
                this, SLOT(OnConnected())));
  CHECK(connect(base_socket_.get(), SIGNAL(disconnected()),
                this, SLOT(OnDisconnected())));
  socket_ = base_socket_.get();
  int port = 995;
  LogDetails("AttemptConnect", true, QString("%1:%2")
             .arg(target_address_.toString()).arg(port));
  base_socket_->connectToHost(target_address_, port);
}

void PopProbe::OnConnected() {
  connect_timer_->stop();
  LogDetails("Connect", true, "");
  // Socket is currently "named" by its IP address.  SSL cert validation
  // will fail with a name match.  That's why we used a Renamable
  // TCP Socket.  So replace the name out from under it with the
  // DNS name.
  base_socket_->SetDnsName(pop_server_);
  StartProbeNegotiateSsl();
}

void PopProbe::OnDisconnected() {
  connect_timer_->stop();
  LogDetails("Disconnected", false, "???");
  ProbeDone(SERVER_CLOSED_PREMATURELY);
}

void PopProbe::OnConnectTimedOut() {
  LogDetails("Connect", false, "TCP Connect timed out");
  ProbeDone(CONNECT_TIMED_OUT);
}

void PopProbe::OnConnectError(QAbstractSocket::SocketError err) {
  connect_timer_->stop();
  switch (err) {
    case QAbstractSocket::ConnectionRefusedError:
      LogDetails("Connect", false, "Connection refused");
      ProbeDone(CONNECTION_REFUSED);
      break;
    case QAbstractSocket::RemoteHostClosedError:
      LogDetails("Connect", false,
                  "Server closed the connection before the banner");
      ProbeDone(SERVER_CLOSED_PREMATURELY);
      break;
    case QAbstractSocket::HostNotFoundError:
      LogDetails("Connect", false,
                  "Connect with IP parameter failed with a resolve error?");
      ProbeDone(RESOLVE_FAILED);
      break;
    case QAbstractSocket::SocketTimeoutError:
      LogDetails("Connect", false, "TCP Connect timed out");
      ProbeDone(CONNECT_TIMED_OUT);
      break;
    case QAbstractSocket::SocketAccessError:
    case QAbstractSocket::SocketResourceError:
    case QAbstractSocket::NetworkError:
    case QAbstractSocket::UnsupportedSocketOperationError:
    case QAbstractSocket::UnknownSocketError:
    default:
      QString error = "no socket";
      if (base_socket_ != NULL) {
        error = base_socket_->errorString();
      }
      LogDetails("Connect", false,
                 QString("Failed with code %1 (%2)")
                        .arg(err).arg(error));
      ProbeDone(SOCKET_ERROR);
  }
}

void PopProbe::StartProbeNegotiateSsl() {
  SetState(NEGOTIATE_SSL);
  QDir dir = QDir::current();
  if (!SetCACertificates(base_socket_.get())) {
    LogDetails("SSL", false, "Trusted CA file not found");
    ProbeDone(POP_SERVER_CAFILE_NOT_FOUND);
    return;
  }
  CHECK(connect(base_socket_.get(),
                SIGNAL(sslErrors(const QList<QSslError> &)),
                this,
                SLOT(OnSslError(const QList<QSslError> &))));
  CHECK(connect(base_socket_.get(), SIGNAL(encrypted()),
                this, SLOT(OnSslEstablished())));
  CHECK(connect(base_socket_.get(),
                SIGNAL(error(QAbstractSocket::SocketError)),
                this,
                SLOT(OnError(QAbstractSocket::SocketError))));

  ssl_negotiate_timer_ = new QTimer(this);
  CHECK(connect(ssl_negotiate_timer_, SIGNAL(timeout()),
                this, SLOT(OnSslTimeout())));
  ssl_negotiate_timer_->setSingleShot(true);
  ssl_negotiate_timer_->start(kTimeoutMs);

  base_socket_->startClientEncryption();
}

void PopProbe::OnError(QAbstractSocket::SocketError err) {
  // In some scenarios, the server sends a response then drops us.
  // In such cases, sometimes we get the OnError first, sometimes we
  // get the DataAvailable first.  Give the app the chance to
  // fire app-level errors before we demote it to the less-specific
  // socket-level error.
  if (reader_) {
    reader_->TryRead();
  }

  CHECK(state_ != CONNECT);
  QString error_string = base_socket_->errorString();
  base_socket_->close();
  if (we_closed_ || fault_ != OK) {
    return;
  }
  LogDetails(StepNameFromState(), false,
             QString("SocketError %1: %2").arg(err).arg(error_string));
  if (state_ == RETR) {
    LogRetrFailure(NULL);
  }
}

void PopProbe::OnSslTimeout() {
  LogDetails("SSL", true, "Negotiation timed out");
  ProbeDone(POP_SERVER_CERT_CANNOT_VALIDATE);
  Cleanup();
}

void PopProbe::OnSslError(const QList<QSslError> &errors) {
  ssl_negotiate_timer_->stop();
  QString debug = base_socket_->peerCertificate().toPem();
  // TODO: Handle all errors of this list
  switch (errors[0].error()) {
    case QSslError::NoError:
      // This shouldn't ever happen, as this function would
      // not have been called in this case.
      LogDetails("SSL", true, "Socket secured");
      socket_ = base_socket_.get();
      break;
    case QSslError::HostNameMismatch:
      LogDetails("SSL", false, "Hostname on cert does not match: " + debug);
      ProbeDone(POP_SERVER_CERT_NAME_MISMATCH);
      break;
    case QSslError::SelfSignedCertificate:
    case QSslError::SelfSignedCertificateInChain:
      LogDetails("SSL", false, "Found self signed cert in cert chain: "
                 + debug);
      ProbeDone(POP_SERVER_CERT_SELFSIGNED);
      break;
    case QSslError::CertificateExpired:
      LogDetails("SSL", false, "Found expired certificate in cert chain: "
                 + debug);
      ProbeDone(POP_SERVER_CERT_EXPIRED);
      break;
    case QSslError::CertificateNotYetValid:
      LogDetails("SSL", false,
                 "Found not yet valid certificate in cert chain: " + debug);
      ProbeDone(POP_SERVER_CERT_NOTYETVALID);
      break;
    case QSslError::UnableToVerifyFirstCertificate:
    default:
      // TODO: Complete the defined SslError cases.
      LogDetails("SSL", false, "Unable to validate cert: " + debug);
      ProbeDone(POP_SERVER_CERT_CANNOT_VALIDATE);
      break;
  }
}

void PopProbe::OnSslEstablished() {
  ssl_negotiate_timer_->stop();
  BeginSecureConversation();
}

void PopProbe::BeginSecureConversation() {
  StartProbeBanner();
}

PopResponseReader *PopProbe::GetReader(bool is_long_response,
                                       bool truncate_response) {
  // do NOT delete the current reader_.  There may be events queued to it
  // on the event queue.  Let it tear down when the probe tears down.
  reader_ = new PopResponseReader(this, socket_, kTimeoutMs,
                                  is_long_response, truncate_response);
  connect(reader_, SIGNAL(ResponseRead(PopResponseReader *)),
          this, SLOT(OnCommandResponseReceived(PopResponseReader *)));
  return reader_;
}

void PopProbe::StartProbeBanner() {
  SetState(BANNER);
  GetReader(false, false)->SendCommand("");
}

const char *PopProbe::StepNameFromState() {
  switch (state_) {
    case INIT:
      return "INIT";
    case CONNECT:
      return "CONNECT";
    case NEGOTIATE_SSL:
      return "NEGOTIATE_SSL";
    case BANNER:
      return "BANNER";
    case CAPA:
      return "CAPA";
    case USER:
      return "USER";
    case PASS:
      return "PASS";
    case STAT:
      return "STAT";
    case UIDL:
      return "UIDL";
    case LIST:
      return "LIST";
    case RETR:
      return "RETR";
    default:
      return "???";
  }
}

void PopProbe::OnCommandResponseReceived(PopResponseReader *reader) {
  if (we_closed_) {
    return;
  }
  QString fault_name = "???";
  PopProbeFault code = POP_SERVER_RESPONSE_MALFORMED;
  switch (reader->response_type()) {
    case POP_RESPONSE_OK:
      ProcessOkResponse(reader);
      return;
    case POP_RESPONSE_ERR:
      ProcessErrResponse(reader);
      return;
    case POP_RESPONSE_MALFORMED:
      fault_name = "Malformed";
      code = POP_SERVER_RESPONSE_MALFORMED;
      break;
    case POP_SOCKET_DROPPED:
      fault_name = "Dropped";
      code = SERVER_CLOSED_PREMATURELY;
      break;
    case POP_RESPONSE_TIMEOUT:
      fault_name = "Timeout";
      code = POP_RESPONSE_TIMED_OUT;
      break;
    default:
      fault_name = QString("Unknown error %1")
        .arg(reader->response_type());
      code = POP_SERVER_RESPONSE_MALFORMED;
  }
  if (state_ == RETR) {
    LogRetrFailure(reader);
  } else {
    LogDetails(StepNameFromState(), false,
               fault_name + ": " + reader->describe());
  }
  ProbeDone(code);
}

void PopProbe::ProcessOkResponse(PopResponseReader *reader) {
  switch (state_) {
    case INIT:            // fall through
    case CONNECT:         // fall through
    case NEGOTIATE_SSL:
      CHECK(false);
      return;
    case BANNER:
      BannerOk(reader);
      return;
    case CAPA:
      CapaOk(reader);
      return;
    case USER:
      UserOk(reader);
      return;
    case PASS:
      PassOk(reader);
      return;
    case STAT:
      StatOk(reader);
      return;
    case UIDL:
      UidlOk(reader);
      return;
    case LIST:
      ListOk(reader);
      return;
    case RETR:
      RetrOk(reader);
  }
}

void PopProbe::ProcessErrResponse(PopResponseReader *reader) {
  switch (state_) {
    case BANNER:
      BannerRejected(reader);  // dispatches error
      return;
    case PASS:
      PassRejected(reader);  // dispatches error
      return;
    case RETR:
      // Logging only.  Fall through for error dispatch.
      LogRetrFailure(reader);
      break;
    default:
      // Logging only.  Fall through for error dispatch
      LogDetails(StepNameFromState(), false, "Rejected: " + reader->describe());
      break;
  }
  switch (state_) {
    case LIST:
      ProbeDone(POP_LIST_FAILED);
      return;
    case UIDL:
      ProbeDone(POP_UIDL_FAILED);
      return;
    case STAT:
      ProbeDone(POP_STAT_FAILED);
      return;
    case RETR:
      ProbeDone(POP_RETR_FAILED);
      return;
    case USER:
      ProbeDone(POP_USER_REJECTED);
      return;
    default:
      ProbeDone(POP_SERVER_RESPONSE_MALFORMED);
  }
}

void PopProbe::BannerOk(PopResponseReader *reader) {
    LogDetails("Banner", true, "Banner OK: " + reader->describe());
    NatCheck(reader);
}

void PopProbe::BannerRejected(PopResponseReader *reader) {
  LogDetails("Banner", false, "Shedding Load: " + reader->describe());
  ProbeDone(POP_SERVER_SHEDDING_LOAD);
}

void PopProbe::NatCheck(PopResponseReader *reader) {
  // If we got a Gmail banner with an IP, check for NAT
  QRegExp re("ready for requests from ([\\d\\.]+)");
  if (re.indexIn(reader->response_line_comment()) >= 0) {
    QString public_ip = re.cap(1);
    if (!public_ip.isEmpty()) {
      QString private_ip = socket_->localAddress().toString();
      bool natted = public_ip != private_ip;
      if (natted) {
        LogDetails("NATCheck", true, "Address translation found");
      } else {
        LogDetails("NATCheck", true, "Address translation not used");
      }
    } else {
      LogDetails("NATCheck", false, "Cannot perform NAT detection");
    }
  }
  if (probe_banner_only_) {
    ProbeDone(OK);
  } else {
    StartProbeCapa();
  }
}

void PopProbe::StartProbeCapa() {
  SetState(CAPA);
  GetReader(true, false)->SendCommand("CAPA");
}

void PopProbe::CapaOk(PopResponseReader *reader) {
  QSet<QString> capas;
  for (QList<QString>::const_iterator i = reader->long_response_lines().begin();
       i != reader->long_response_lines().end(); ++i) {
    capas.insert(i->toUpper().trimmed());
  }
  if (!capas.contains("USER")) {
    LogDetails("CAPA", false, "USER Capability not offered");
    ProbeDone(POP_CAPA_USER_MISSING);
    return;
  } else {
    LogDetails("CAPA", true, "");
    StartProbeUser();
  }
}

void PopProbe::StartProbeUser() {
  SetState(USER);
  QString cmd = "USER " + username_;
  GetReader(false, false)->SendCommand(cmd);
}

void PopProbe::UserOk(PopResponseReader *reader) {
  if (reader->response_line_comment().contains("empty mailbox")) {
    LogDetails("USER", false, "Empty Mode: " + reader->describe());
    ProbeDone(POP_USER_BACKEND_OFFLINE);
  } else {
    LogDetails("USER", true, "Accepted: " + reader->describe());
    StartProbePass();
  }
}

void PopProbe::StartProbePass() {
  SetState(PASS);
  QString cmd = "PASS " + password_;
  GetReader(false, false)->SendCommand(cmd);
}

void PopProbe::PassOk(PopResponseReader *reader) {
  QString comment = reader->response_line_comment();
  if (comment.contains("empty")) {
    LogDetails("PASS", false, "Empty mode: " + reader->describe());
    ProbeDone(POP_LOGIN_FAILED_TEMP_SYSTEM_FAILURE);
  } else {
    LogDetails("PASS", true, reader->describe());
    StartProbeStat();
  }
}

void PopProbe::PassRejected(PopResponseReader *reader) {
  QString extended = reader->extended_code().toUpper();
  QString comment = reader->response_line_comment();

  if ("SYS/TEMP" == extended) {
    LogDetails("PASS", false, "Temporary system failure failed login: "
                              + reader->describe());
    ProbeDone(POP_LOGIN_FAILED_TEMP_SYSTEM_FAILURE);
  } else if ("SYS/PERM" == extended) {
    if (comment.contains("not enabled")) {
      LogDetails("PASS", false, "User has not enabled POP access: "
                 + reader->describe());
      ProbeDone(POP_NOT_ENABLED_FOR_USER);
    } else if (comment.contains("Lockdown")) {
      LogDetails("PASS", false, "User locked out for abuse: "
                 + reader->describe());
      ProbeDone(POP_LOGIN_FAILED_ABUSE);
    } else {
      LogDetails("PASS", false, "Permanent system failure failed login: "
                 + reader->describe());
      ProbeDone(POP_LOGIN_FAILED_PERM_SYSTEM_FAILURE);
    }
  } else if ("AUTH" == extended) {
    if (comment.contains("from your location")) {
      LogDetails("PASS", false, "Geographic lockout: "
                 + reader->describe());
      ProbeDone(POP_LOGIN_FAILED_GEO_LOCKOUT);
    } else {
      LogDetails("PASS", false, "Bad credentials or lockout: "
                 + reader->describe());
      ProbeDone(POP_LOGIN_FAILED_BAD_CREDS);
    }
  } else {
    LogDetails("PASS", false, "Login failed, no extended code: "
                              + reader->describe());
    ProbeDone(POP_LOGIN_FAILED_BAD_CREDS);
  }
}

void PopProbe::StartProbeStat() {
  SetState(STAT);
  GetReader(false, false)->SendCommand("STAT");
}

void PopProbe::StatOk(PopResponseReader *reader) {
  QString response = reader->response_line_comment();
  QStringList parts = response.split(" ");
  if (parts.size() != 2) {
    LogDetails("STAT", false, "Malformed(2): " + reader->describe());
    ProbeDone(POP_SERVER_RESPONSE_MALFORMED);
    return;
  }
  int num_msgs = parts[0].toInt();
  // if we ever make real use of num_bytes, make sure to upgrade to an int64.
  int num_bytes = parts[1].toInt();
  LogDetails("STAT", true, QString("Mailbox has %1 msgs with %2 bytes")
             .arg(num_msgs).arg(num_bytes));
  msgs_in_mailbox_ = num_msgs;
  bytes_expected_ = num_bytes;
  StartProbeUidl();
}

void PopProbe::StartProbeUidl() {
  SetState(UIDL);
  GetReader(true, false)->SendCommand("UIDL");
}

void PopProbe::UidlOk(PopResponseReader *reader) {
  LogDetails("UIDL", true, "");
  uidls_.clear();
  QList<QString> lines = reader->long_response_lines();
  for (QList<QString>::iterator i = lines.begin();
       i != lines.end(); ++i) {
    QStringList parts = i->split(" ");
    if (parts.size() != 2) {
      LogDetails("UIDL", false, "malformed line: " + *i);
      ProbeDone(POP_SERVER_RESPONSE_MALFORMED);
      return;
    }
    uidls_.push_back(parts[1]);
  }
  StartProbeList();
}

void PopProbe::StartProbeList() {
  SetState(LIST);
  GetReader(true, false)->SendCommand("LIST");
}

void PopProbe::ListOk(PopResponseReader *reader) {
  QList<QString> lines = reader->long_response_lines();
  for (QList<QString>::iterator i = lines.begin();
       i != lines.end(); ++i) {
    QStringList parts = i->split(" ");
    if (parts.size() != 2) {
      LogDetails("LIST", false, "malformed line: " + *i);
      ProbeDone(POP_SERVER_RESPONSE_MALFORMED);
      return;
    }
    QString z;
    lengths_.push_back(parts[1].toInt());
  }
  LogDetails("LIST", true, "");

  // Start timers for retrieve here because we want to repeatedly
  // call StartProbeRetr().
  fetch_timer_ = new QTime();
  fetch_timer_->start();
  msgs_retrieved_ = 0;
  if (msgs_in_mailbox_ > 0) {
    StartProbeRetr();
  } else {
    LogDetails("RETR", true, "No mail to retrieve");
    ProbeDone(OK);
  }
}

void PopProbe::StartProbeRetr() {
  SetState(RETR);
  QString cmd = "RETR " + IntToString(msgs_retrieved_+ 1);
  GetReader(true, true)->SendCommand(cmd);
}

void PopProbe::RetrOk(PopResponseReader * /*reader*/) {
  ScheduleNextRetr();
}

// logs more details about what we were retrieving extracted from UIDL
// and LIST commands to help us fix this on the backend.
void PopProbe::LogRetrFailure(PopResponseReader *reader) {
  int length = 0;
  QString uidl = "???";
  if (msgs_retrieved_ < lengths_.size()) {
    length = lengths_[msgs_retrieved_];
    uidl = uidls_[msgs_retrieved_];
  }
  QString msg_desc = QString("Message #%1 of %2, UID %3, size %4")
                     .arg(msgs_retrieved_ + 1)
                     .arg(msgs_in_mailbox_).arg(uidl)
                     .arg(length);
  PopProbeFault fault = POP_SERVER_RESPONSE_MALFORMED;
  QString verb = "????";
  if (!reader) {
    fault = SERVER_CLOSED_PREMATURELY;
    verb = "Socket error";
  } else {
    switch (reader->response_type()) {
      case POP_RESPONSE_OK:
        // should go to RetrOk(), not here.
        CHECK(false);
        break;
      case POP_RESPONSE_ERR:
        // treat err on RETR as a malformed response
        fault = POP_SERVER_RESPONSE_MALFORMED;
        verb = "Failed";
        break;
      case POP_RESPONSE_MALFORMED:
        fault = POP_SERVER_RESPONSE_MALFORMED;
        verb = "Malformed";
        break;
      case POP_SOCKET_DROPPED:
        fault = SERVER_CLOSED_PREMATURELY;
        verb = "Connection dropped";
        break;
      case POP_RESPONSE_TIMEOUT:
        fault = POP_RESPONSE_TIMED_OUT;
        verb = "Timeout";
        break;
    }
  }
  LogDetails("RETR", false, QString("%1 for %2: %3")
            .arg(verb).arg(msg_desc)
            .arg(reader == NULL ? "no reader" : reader->describe()));
  ProbeDone(fault);
}

void PopProbe::ScheduleNextRetr() {
  bytes_retrieved_ += lengths_[msgs_retrieved_];
  if (!pull_all_mail_) {
    LogDetails("RETR1ST", true,
                "Fetching first message succeeded.  Not checking the rest");
  }
  emit ProgressMade(bytes_retrieved_, bytes_expected_);

  ++msgs_retrieved_;
  bool fetch_more = pull_all_mail_ && (msgs_in_mailbox_ > msgs_retrieved_);

  if (!fetch_more) {
    int millis = fetch_timer_->elapsed();
    // kilo BITS per second
    double throughput_in_kbps = 0.0;
    if (millis != 0) {
      const int kBitsPerByte = 8;
      const double kMillisPerSecond = 1000.0;
      const double kKilo = 1024.0;
      double seconds_elapsed = millis / kMillisPerSecond;
      double bits_retrieved = kBitsPerByte * bytes_retrieved_;
      double bits_per_second = bits_retrieved / seconds_elapsed;
      throughput_in_kbps = bits_per_second / kKilo;
    }
    LogDetails("RETR", true,
              QString("Fetched %1 messages totalling %2 bytes in %3 ms "
                      "(%4 kbps estimated)")
              .arg(msgs_retrieved_).arg(bytes_retrieved_).arg(millis)
              // Show throughput with at most 3 digits after the decimal
              .arg(throughput_in_kbps, 0, 'f', 3));

    // Do NOT issue QUIT due to Gmail EXPIRES 0 semantics on RETR.
    // We need an abrupt termination.

    // Normal termination case for PopProbe.
    ProbeDone(OK);
  } else {
    StartProbeRetr();
  }
}


PopResponseReader::PopResponseReader(QObject *parent,
                                     RenamableSslSocket *src,
                                     int timeout_in_millis,
                                     bool long_response,
                                     bool truncate_response)
  : QObject(parent),
    socket_(src), response_lines_(), response_type_(POP_RESPONSE_MALFORMED),
    extended_code_(),
    response_comment_(), anything_read_(false),
    reader_(src, timeout_in_millis),
    long_response_(long_response),
    truncate_response_(truncate_response) {
  CHECK(connect(&reader_, SIGNAL(DataReady(LineReadResult)),
                this, SLOT(DataAvailable(LineReadResult))));
}

PopResponseReader::~PopResponseReader() {
}

void PopResponseReader::DataAvailable(LineReadResult result) {
  switch (result) {
    case DATA_READ:
      TryRead();
      break;
    case TIMEOUT:
      Done(POP_RESPONSE_TIMEOUT);
      break;
    case SOCKET_DROPPED:
      Done(POP_SOCKET_DROPPED);
      break;
  }
}

QString PopResponseReader::describe() {
  QString rval;
  QTextStream r(&rval, QIODevice::WriteOnly);
  // each branch of this switch statement must open a <code> tag:
  switch (response_type_) {
    case POP_RESPONSE_OK:
      r << "<code>+OK ";
      break;
    case POP_RESPONSE_ERR:
      r << "<code>-ERR ";
      break;
    case POP_RESPONSE_MALFORMED:
      r << "Malformed: <code>" << response_comment_;
      break;
    case POP_SOCKET_DROPPED:
      r << "ConnDrop: <code>";
      // Fall out and dump the rest, just in case.
      break;
    case POP_RESPONSE_TIMEOUT:
      r << "Timeout: <code>";
      // Fall out and dump the rest, just in case
      break;
  }

  if (!extended_code_.isEmpty()) {
    r << "[" << extended_code_ << "] ";
  }
  r << response_comment_ << "<br>\n";
  // show the first two and the last two lines of the response
  for (int i = 0; i < 2 && i < response_lines_.size(); ++i) {
    if (i > 0) {
      r << "<br>";
    }
    r << Qt::escape(response_lines_[i]);
  }
  if (response_lines_.size() > 4) {
    r << "<br>...\n";
  }
  for (int i = response_lines_.size() - 2; i < response_lines_.size();
        ++i) {
    if (i < 2) {
      continue;
    }
    r << "<br>" << Qt::escape(response_lines_[i]);
  }
  r << "</code>";
  return rval;
}

void PopResponseReader::Done(PopResponseType code) {
  response_type_ = code;
  reader_.Cleanup();
  emit ResponseRead(this);
}

void PopResponseReader::SendCommand(const QString &cmd) {
  if (!cmd.isEmpty()) {
    Probe::Debug("POP sending line: %s\n", qPrintable(cmd));
    QString to_write(cmd + "\r\n");
    socket_->write(qPrintable(to_write));
    socket_->flush();
  }
  RunWhenEventQueueEmpties(this, SLOT(TryRead()));
}

void PopResponseReader::TryRead() {
  QString line;
  while (reader_.NonblockingRead(&line)) {
    ProcessLine(line);
  }
}

void PopResponseReader::ProcessLine(const QString &line) {
  Probe::Debug("POP received line: %s", qPrintable(line));
  if (!anything_read_) {
    ProcessFirstLine(line);
  } else {
    ProcessFollowupLine(line);
  }
}

void PopResponseReader::ProcessFirstLine(const QString &line) {
  anything_read_ = true;
  QRegExp first_line("^(\\+OK|-ERR)(?: \\[([^\\]]+)\\] )?(.*)\\s*$",
                    Qt::CaseInsensitive);
  int match_loc = first_line.indexIn(line);
  if (0 != match_loc) {
    response_comment_ = line;
    Done(POP_RESPONSE_MALFORMED);
    return;
  }
  bool succeeded = "+OK" == first_line.cap(1).toUpper();
  extended_code_ = first_line.cap(2);
  response_comment_ = first_line.cap(3).trimmed();

  if (!succeeded) {
    Done(POP_RESPONSE_ERR);
    return;
  } else {
    // okay.  First line read.
    if (!long_response_) {
      Done(POP_RESPONSE_OK);
    } else {
      // we need more lines of input
    }
  }
}

void PopResponseReader::ProcessFollowupLine(const QString &line) {
  if (line.trimmed() == ".") {
    Done(POP_RESPONSE_OK);
  } else {
    // describe() will only need the first few and the last two
    // lines of a message. In order to save memory, do not keep
    // the entire message here, but only a few lines.
    if (response_lines_.size() == 10 && truncate_response_) {
      response_lines_.insert(9, response_lines_.value(10));
      response_lines_.insert(10, line);
    } else {
      response_lines_.append(line);
    }
  }
}

}  // namespace popdiag
